@bridge-ds/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +27 -1
  2. package/bridge.config.schema.json +23 -0
  3. package/dist/audit/index.d.ts +1 -0
  4. package/dist/audit/index.js +269 -23
  5. package/dist/audit/index.js.map +1 -1
  6. package/dist/audit/missing-pieces.d.ts +43 -0
  7. package/dist/config.d.ts +28 -0
  8. package/dist/docs-proposal.d.ts +58 -10
  9. package/dist/figma-push-github.d.ts +143 -0
  10. package/dist/figma-push.d.ts +204 -0
  11. package/dist/github-client.d.ts +15 -1
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.js +1569 -3179
  14. package/dist/index.js.map +1 -1
  15. package/dist/node/git-local.d.ts +15 -0
  16. package/dist/node/index.d.ts +1 -0
  17. package/dist/node/index.js +201 -10
  18. package/dist/node/index.js.map +1 -1
  19. package/dist/node/suggestion-apply-node.d.ts +69 -0
  20. package/dist/{component-spec-SKuywZqr.js → prod-branch-BdMPKffE.js} +1134 -1438
  21. package/dist/prod-branch-BdMPKffE.js.map +1 -0
  22. package/dist/prod-branch-github.d.ts +160 -0
  23. package/dist/prod-branch.d.ts +267 -0
  24. package/dist/{report-iPSnsjYi.js → report-DOnKUw8G.js} +24 -24
  25. package/dist/{report-iPSnsjYi.js.map → report-DOnKUw8G.js.map} +1 -1
  26. package/dist/suggestion-apply-ByBEYtct.js +4802 -0
  27. package/dist/suggestion-apply-ByBEYtct.js.map +1 -0
  28. package/dist/suggestion-apply.d.ts +150 -0
  29. package/dist/suggestion-review.d.ts +70 -0
  30. package/dist/suggestions-github.d.ts +131 -0
  31. package/dist/suggestions.d.ts +236 -0
  32. package/dist/token-proposal.d.ts +123 -0
  33. package/package.json +1 -1
  34. package/dist/component-spec-SKuywZqr.js.map +0 -1
  35. package/dist/layered-BnbmToGs.js +0 -500
  36. package/dist/layered-BnbmToGs.js.map +0 -1
package/README.md CHANGED
@@ -36,7 +36,8 @@ also exported as `@bridge-ds/core/bridge.config.schema.json`) for the plugin, th
36
36
  server. Canonical form: `repo`, `tokens` (`root`, `include`, `exclude`, `tiers`, ordered `layers` with `{mode}`
37
37
  placeholders and `optional`), `modes` (name -> `{ values, default, parity }`), `contrast` (`path`, `modes`),
38
38
  `components`, `docs`, `changesets`, `cssVar`, `governance` (`protectedBranches`, `branchPrefix`, `labels`,
39
- `guardrails`, `owners: { design, tech }`). The older flat form (`tokens.primitives`, `modes.brands`...) still loads.
39
+ `guardrails`, `owners: { design, tech }`), `changes` (`"suggest"`, the default, or `"direct"`) and `storybook`
40
+ (`{ url }`, https). The older flat form (`tokens.primitives`, `modes.brands`...) still loads.
40
41
 
41
42
  ```ts
42
43
  const CONFIG_FILE = 'bridge.config.json';
@@ -50,10 +51,13 @@ interface BridgeConfig {
50
51
  docs; changesets: { dir; package } | undefined; changeset?: { package };
51
52
  cssVar: { prefix; separator };
52
53
  governance: { protectedBranches; branchPrefix; labels; guardrails; owners: { design: string[]; tech: string[] } };
54
+ changes: 'suggest' | 'direct'; // how changes reach the code (default 'suggest')
55
+ storybook?: { url: string };
53
56
  }
54
57
  function defineConfig(input): BridgeConfig; function loadConfig(source): BridgeConfig; // ConfigError { problems }
55
58
  function toConfigJson(config): object; // canonical JSON (what `bridge init` writes)
56
59
  function protectedBranchesOf(config); function baseBranchOf(config); function tokensPath(config, rel?);
60
+ function changesMode(config): 'suggest' | 'direct'; function storybookUrl(config): string | undefined; // storybook.url, else previews.production.storybook
57
61
  ```
58
62
 
59
63
  ## governance
@@ -257,6 +261,28 @@ loadRepoFiles(client, commitSha, fallbackTokensPath): Promise<{ files; contrastC
257
261
  Reads and content-addressed writes retry on network errors, 5xx and secondary rate limits; errors
258
262
  become `GitHubError { status, message, detail }` with plain-language messages.
259
263
 
264
+ ## suggestions (suggestions.ts, suggestion-review.ts, suggestions-github.ts)
265
+
266
+ A change filed as a GitHub issue instead of a branch, so an owner needs only Issues write. Readable Markdown, then a
267
+ collapsed JSON payload (`bridge.suggestion/1`) with the sha256 of its canonical form in a marker; token changes are
268
+ edits (paths and values), other files whole contents with git blob shas before and after. Contents that do not fit
269
+ the body (65,536 characters) go to continuation comments, 8 at most. Reviews are comments bound to the payload sha
270
+ and counted only from the configured owners.
271
+
272
+ ```ts
273
+ composeSuggestionIssue(payload, { labels?, assignees?, state?, details? }): { title; body; labels; assignees; parts: string[]; sha };
274
+ parseSuggestionIssue(body, { comments?, author? }): { ok: true; payload; sha; markedSha; edited; parts } | { ok: false; reason; message };
275
+ suggestionPayloadSha(payload); canonicalJson(value); sha256Hex(text); gitBlobSha(text);
276
+ suggestionLabels(payload, { state?, labels? }); suggestionIssueTitle(payload); suggestionStatus({ state, stateReason?, linkedPrs?, review?, applied?, outdated? });
277
+ suggestionPathProblems(payload, config): string[]; unsafePathProblem(path): string | null;
278
+ summarizeSuggestionApprovals(comments, { owners, rights?, teams? }, { payloadSha, author? }); suggestionReviewState(tier, approvals);
279
+ composeSuggestionReviewComment(record); parseSuggestionReviewComment(body); composeSuggestionAppliedComment(record); parseSuggestionAppliedComment(body);
280
+ // GitHub (Issues read and write only)
281
+ createSuggestionOnGitHub(gh, payload, opts); listSuggestionsOnGitHub(gh, { state? }); getSuggestionOnGitHub(gh, n, gov, { outdated? });
282
+ reviewSuggestionOnGitHub(gh, n, { event, role?, message?, payloadSha? }, gov); linkSuggestionPrOnGitHub(gh, n, { pr?, branch?, commit?, payloadSha });
283
+ declineSuggestionOnGitHub(gh, n, reason, by?); syncSuggestionStateLabel(gh, n, labels, status);
284
+ ```
285
+
260
286
  ## history (history.ts, history-github.ts, history-demo.ts)
261
287
 
262
288
  What changed in the tokens, when, who changed it and why, and how to undo it safely.
@@ -650,6 +650,29 @@
650
650
  }
651
651
  }
652
652
  },
653
+ "changes": {
654
+ "enum": [
655
+ "suggest",
656
+ "direct"
657
+ ],
658
+ "default": "suggest",
659
+ "description": "How Bridge sends a change to the code. suggest (default): Bridge files a GitHub issue that holds the exact change (a suggestion); owners approve it and a developer applies it with their own access. direct: Bridge opens branches and pull requests itself, which needs Contents and Pull requests write. This file is a guardrail, so switching modes is a Tier 3 change."
660
+ },
661
+ "storybook": {
662
+ "type": "object",
663
+ "additionalProperties": false,
664
+ "required": [
665
+ "url"
666
+ ],
667
+ "description": "The deployed Storybook Bridge reads the component list and previews from. Absent: previews.production.storybook when it is set.",
668
+ "properties": {
669
+ "url": {
670
+ "type": "string",
671
+ "pattern": "^https://",
672
+ "description": "Root URL of the deployed Storybook (where index.json and iframe.html are served), https only."
673
+ }
674
+ }
675
+ },
653
676
  "visualImpact": {
654
677
  "type": "object",
655
678
  "additionalProperties": false,
@@ -10,3 +10,4 @@ export * from './report';
10
10
  export * from './style';
11
11
  export * from './ignore';
12
12
  export * from './markup';
13
+ export * from './missing-pieces';
@@ -1,35 +1,278 @@
1
- import { A, a, b, C, D, c, F, I, L, d, M, N, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, B, E, G, H, J, K, O, P, Q, R, S, T, U, V, W, X, Y, Z } from "../report-iPSnsjYi.js";
1
+ import { i as isAuditable, a as auditFile } from "../report-DOnKUw8G.js";
2
+ import { A, b, c, C, D, d, F, I, L, e, M, N, f, g, h, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, B, E, G, H, J, K, O, P, Q, R, S, T, U, V, W, X, Y, Z } from "../report-DOnKUw8G.js";
3
+ import { p as pieceId, a as proposedTokenPath, t as tokenLabel } from "../prod-branch-BdMPKffE.js";
4
+ const lines = (text) => text.split(/\r?\n/);
5
+ function lineBag(text) {
6
+ const bag = /* @__PURE__ */ new Map();
7
+ for (const l2 of text ? lines(text) : []) {
8
+ const k2 = l2.trim();
9
+ if (k2) bag.set(k2, (bag.get(k2) ?? 0) + 1);
10
+ }
11
+ return bag;
12
+ }
13
+ const offsetOf = (text, line, column) => {
14
+ let off = 0;
15
+ const ls = text.split("\n");
16
+ for (let i = 0; i < line - 1 && i < ls.length; i++) off += ls[i].length + 1;
17
+ return off + Math.max(0, column - 1);
18
+ };
19
+ function enclosingTag(text, offset) {
20
+ for (let i = offset - 1; i >= Math.max(0, offset - 4e3); i--) {
21
+ if (text[i] !== "<" || !/[A-Za-z]/.test(text[i + 1] ?? "")) continue;
22
+ let depth = 0;
23
+ let closed = false;
24
+ for (let j2 = i + 1; j2 < offset; j2++) {
25
+ const c2 = text[j2];
26
+ if (c2 === "{") depth++;
27
+ else if (c2 === "}") depth--;
28
+ else if (c2 === ">" && depth <= 0 && text[j2 - 1] !== "=") {
29
+ closed = true;
30
+ break;
31
+ }
32
+ }
33
+ if (closed) return null;
34
+ const m2 = /^<([A-Za-z][\w.]*)/.exec(text.slice(i));
35
+ return m2 ? { name: m2[1], start: i } : null;
36
+ }
37
+ return null;
38
+ }
39
+ function childText(text, tagStart) {
40
+ let depth = 0;
41
+ for (let j2 = tagStart + 1; j2 < text.length; j2++) {
42
+ const c2 = text[j2];
43
+ if (c2 === "{") depth++;
44
+ else if (c2 === "}") depth--;
45
+ else if (c2 === ">" && depth <= 0 && text[j2 - 1] !== "=") {
46
+ if (text[j2 - 1] === "/") return null;
47
+ const rest = /^([^<{]*)</.exec(text.slice(j2 + 1));
48
+ const t2 = rest?.[1]?.trim() ?? "";
49
+ return t2 && t2.split(/\s+/).length <= 3 && t2.length <= 30 ? t2 : null;
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ function ruleClasses(text, offset) {
55
+ const open = text.lastIndexOf("{", offset);
56
+ if (open < 0) return [];
57
+ const prev = Math.max(text.lastIndexOf("}", open), text.lastIndexOf(";", open));
58
+ const selector = text.slice(prev + 1, open);
59
+ return [...selector.matchAll(/\.([A-Za-z_][\w-]*)/g)].map((m2) => m2[1]);
60
+ }
61
+ const GENERIC = /* @__PURE__ */ new Set(["status", "badge", "tag", "pill", "chip", "style", "styles", "root", "wrapper", "container", "item", "label", "text", "color", "bg", "is", "has", "variant", "tone", "custom", "new"]);
62
+ function nameFromClass(cls, component) {
63
+ const words = cls.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).map((w2) => w2.toLowerCase()).filter(Boolean);
64
+ const comp = component?.toLowerCase();
65
+ const kept = words.filter((w2) => w2 !== comp && !GENERIC.has(w2));
66
+ return kept[kept.length - 1] ?? words[words.length - 1] ?? cls;
67
+ }
68
+ const ASPECT = { color: "tone", radius: "shape", spacing: "size", size: "size", "font-size": "size", "line-height": "size" };
69
+ function aspectOf(values) {
70
+ for (const a of ["tone", "shape", "size"]) if (values.some((v2) => ASPECT[v2.category] === a)) return a;
71
+ return "style";
72
+ }
73
+ const norm = (f2) => f2.category === "color" ? f2.value.toLowerCase() : f2.value.replace(/\s+/g, "");
74
+ function nearestOf(s2) {
75
+ if (!s2) return void 0;
76
+ return { token: s2.token, cssVar: s2.cssVar, value: s2.value, level: s2.level, label: tokenLabel(s2.token) };
77
+ }
78
+ const TOKEN_TYPE = { color: "color", duration: "duration", "font-weight": "fontWeight", "line-height": "number", "z-index": "number", "font-family": "fontFamily", shadow: "shadow", easing: "cubicBezier" };
79
+ const valueTitle = (category, value) => {
80
+ const v2 = category === "color" && /^#[0-9a-f]+$/i.test(value) ? value.toUpperCase() : value;
81
+ const word = { color: "color", radius: "radius", spacing: "spacing", size: "size", "font-size": "text size", "font-weight": "font weight", "line-height": "line height", shadow: "shadow", duration: "duration", easing: "easing", "z-index": "layer", "border-width": "border width", "font-family": "font" };
82
+ return `New ${word[category] ?? category} ${v2}`;
83
+ };
84
+ function missingPieces(input) {
85
+ const { ds } = input;
86
+ const files = input.files.filter((f2) => f2.after !== null && isAuditable(f2.path));
87
+ const fixes = [];
88
+ const variants = /* @__PURE__ */ new Map();
89
+ const tokens = /* @__PURE__ */ new Map();
90
+ const foreign = /* @__PURE__ */ new Map();
91
+ const classUse = (cls) => {
92
+ const re = new RegExp(`<([A-Z][\\w.]*)\\b[^<]*?styles(?:\\.${cls}\\b|\\[['"]${cls}['"]\\])`);
93
+ for (const f2 of files) {
94
+ const m2 = re.exec(f2.after);
95
+ if (m2 && ds.components.has(m2[1].split(".")[0])) return { component: m2[1].split(".")[0], text: childText(f2.after, m2.index) };
96
+ }
97
+ return null;
98
+ };
99
+ for (const f2 of files) {
100
+ const after = f2.after;
101
+ const report = auditFile(f2.path, after, ds);
102
+ const before = lineBag(f2.before);
103
+ const afterLines = lines(after);
104
+ const added = (line) => {
105
+ const k2 = afterLines[line - 1]?.trim() ?? "";
106
+ const n2 = before.get(k2) ?? 0;
107
+ if (n2 > 0) {
108
+ before.set(k2, n2 - 1);
109
+ return false;
110
+ }
111
+ return true;
112
+ };
113
+ const addedLines = /* @__PURE__ */ new Map();
114
+ const isNew = (line) => {
115
+ if (!addedLines.has(line)) addedLines.set(line, added(line));
116
+ return addedLines.get(line);
117
+ };
118
+ const css = report.lang === "css" || report.lang === "scss" || report.lang === "less";
119
+ for (const finding of report.findings) {
120
+ if (!isNew(finding.line)) continue;
121
+ const s2 = finding.suggestion;
122
+ const at = { file: f2.path, line: finding.line };
123
+ if (s2?.level === "exact" || s2?.level === "high" && finding.category !== "color") {
124
+ fixes.push({ ...at, what: finding.value, use: s2.token });
125
+ continue;
126
+ }
127
+ const offset = offsetOf(after, finding.line, finding.column);
128
+ let component = null;
129
+ let name = null;
130
+ if (css) {
131
+ const classes = ruleClasses(after, offset);
132
+ for (const cls of [...classes].reverse()) {
133
+ const use = classUse(cls);
134
+ if (use) {
135
+ component = use.component;
136
+ name = nameFromClass(cls, use.component);
137
+ break;
138
+ }
139
+ }
140
+ if (!component && classes.length) name = nameFromClass(classes[classes.length - 1]);
141
+ } else {
142
+ const tag = enclosingTag(after, offset);
143
+ const root = tag?.name.split(".")[0];
144
+ if (tag && root && ds.components.has(root)) {
145
+ component = root;
146
+ name = childText(after, tag.start);
147
+ const cls = /className=\{?["'`]?([\w-]+)/.exec(after.slice(tag.start, offset))?.[1];
148
+ if (!name && cls) name = nameFromClass(cls, root);
149
+ }
150
+ }
151
+ const hit = { ...at, finding };
152
+ if (component) {
153
+ const label = (name ?? "custom").toLowerCase();
154
+ const key2 = `${component}|${label}`;
155
+ const v2 = variants.get(key2) ?? { component, name: label, hits: [] };
156
+ v2.hits.push(hit);
157
+ variants.set(key2, v2);
158
+ continue;
159
+ }
160
+ const key = `${finding.category}|${norm(finding)}`;
161
+ const t2 = tokens.get(key) ?? { category: finding.category, value: finding.value, name, hits: [] };
162
+ if (!t2.name && name) t2.name = name;
163
+ t2.hits.push(hit);
164
+ tokens.set(key, t2);
165
+ }
166
+ for (const c2 of report.components) {
167
+ if (!isNew(c2.line)) continue;
168
+ if (c2.suggestion) {
169
+ fixes.push({ file: f2.path, line: c2.line, what: c2.name, use: c2.suggestion });
170
+ continue;
171
+ }
172
+ if (c2.kind !== "foreign") continue;
173
+ const e2 = foreign.get(c2.name) ?? { finding: c2, hits: [] };
174
+ e2.hits.push({ file: f2.path, line: c2.line });
175
+ foreign.set(c2.name, e2);
176
+ }
177
+ }
178
+ const where = (hits) => hits.slice(0, 5).map((h2) => ({ file: h2.file, line: h2.line }));
179
+ const pieces = [];
180
+ for (const v2 of variants.values()) {
181
+ const values = [];
182
+ for (const h2 of v2.hits) {
183
+ const pv = { category: h2.finding.category, ...h2.finding.property ? { property: h2.finding.property } : {}, value: h2.finding.value };
184
+ if (!values.some((x2) => x2.category === pv.category && x2.property === pv.property && norm(x2) === norm(pv))) values.push(pv);
185
+ }
186
+ const aspect = aspectOf(values);
187
+ const bg = v2.hits.find((h2) => /background/.test(h2.finding.property ?? ""))?.finding.suggestion;
188
+ const nearest = nearestOf(bg ?? v2.hits.find((h2) => h2.finding.suggestion)?.finding.suggestion);
189
+ const shown = values.map((x2) => `${x2.property ?? x2.category} ${x2.value}`).join(", ");
190
+ pieces.push({
191
+ id: pieceId([v2.component, v2.name, aspect]),
192
+ kind: "variant",
193
+ title: `${v2.component}: ${v2.name} ${aspect}`,
194
+ summary: `${v2.component} restyled by hand (${shown}).${nearest ? ` ${nearest.level === "high" ? "It looks a lot like" : "The closest in the library is"} ${nearest.label}.` : ""}`,
195
+ name: v2.name,
196
+ component: v2.component,
197
+ aspect,
198
+ values,
199
+ ...nearest ? { nearest } : {},
200
+ where: where(v2.hits)
201
+ });
202
+ }
203
+ for (const [name, e2] of foreign) {
204
+ pieces.push({
205
+ id: pieceId(["component", name]),
206
+ kind: "component",
207
+ title: `New component: ${name}`,
208
+ summary: `${name} comes from ${e2.finding.library}, and the library has nothing like it.`,
209
+ name,
210
+ component: name.split(".").pop(),
211
+ values: [],
212
+ where: where(e2.hits)
213
+ });
214
+ }
215
+ for (const t2 of tokens.values()) {
216
+ const nearest = nearestOf(t2.hits[0].finding.suggestion);
217
+ const pname = t2.name ?? `${t2.category === "color" ? "custom" : t2.category}`;
218
+ const path = proposedTokenPath(nearest?.token, pname, t2.category);
219
+ const tokenFiles = nearest && input.set ? [...new Set(input.set.defs.filter((d2) => d2.path === nearest.token).map((d2) => d2.file))] : [];
220
+ const n2 = t2.hits.length;
221
+ pieces.push({
222
+ id: pieceId([t2.category, t2.name ?? t2.value.replace(/^#/, "")]),
223
+ kind: "token",
224
+ title: valueTitle(t2.category, t2.value),
225
+ summary: `Written by hand in ${n2 === 1 ? "one place" : `${n2} places`}.${nearest ? ` ${nearest.level === "high" ? "It looks a lot like" : "The closest in the library is"} ${nearest.label} (${nearest.value}).` : " The library has nothing close."}`,
226
+ name: pname,
227
+ values: [{ category: t2.category, ...t2.hits[0].finding.property ? { property: t2.hits[0].finding.property } : {}, value: t2.value }],
228
+ ...nearest ? { nearest } : {},
229
+ where: where(t2.hits),
230
+ token: { path, type: TOKEN_TYPE[t2.category] ?? "dimension", value: t2.value, files: tokenFiles }
231
+ });
232
+ }
233
+ const order = { variant: 0, component: 1, token: 2 };
234
+ const seen = /* @__PURE__ */ new Set();
235
+ const out = pieces.sort((a, b2) => order[a.kind] - order[b2.kind]).map((p2) => {
236
+ let id = p2.id;
237
+ for (let i = 2; seen.has(id); i++) id = `${p2.id}-${i}`;
238
+ seen.add(id);
239
+ return id === p2.id ? p2 : { ...p2, id };
240
+ });
241
+ return { pieces: out, fixes, files: files.length };
242
+ }
2
243
  export {
3
244
  A as AUDIT_EXTENSIONS,
4
- a as AUDIT_IGNORE_DIRS,
5
- b as AUDIT_SCHEMA_VERSION,
245
+ b as AUDIT_IGNORE_DIRS,
246
+ c as AUDIT_SCHEMA_VERSION,
6
247
  C as COLOR_LITERAL_RE,
7
248
  D as DEFAULT_NATIVE_ELEMENTS,
8
- c as DesignSystemIndex,
249
+ d as DesignSystemIndex,
9
250
  F as FINDING_CATEGORIES,
10
251
  I as IGNORE_DIRECTIVE,
11
252
  L as LOOKALIKE_ATTRS,
12
- d as LOOKALIKE_CLASSES,
253
+ e as LOOKALIKE_CLASSES,
13
254
  M as MARKUP_EXTENSIONS,
14
255
  N as NAMED_COLORS,
15
- e as applyAllowlist,
16
- f as applyIgnoreComments,
17
- g as auditCssText,
18
- h as auditFile,
19
- i as auditMarkdown,
20
- j as blankComments,
21
- k as buildAuditReport,
22
- l as classifyModule,
23
- m as classifyTag,
24
- n as compareWithBaseline,
25
- o as cssPropertyOf,
26
- p as deltaE,
27
- q as domRendererComponent,
28
- r as dsEquivalent,
29
- s as historyPoint,
30
- t as ignoreDirectives,
31
- u as importsOf,
32
- v as isAuditable,
256
+ f as applyAllowlist,
257
+ g as applyIgnoreComments,
258
+ h as auditCssText,
259
+ auditFile,
260
+ j as auditMarkdown,
261
+ k as blankComments,
262
+ l as buildAuditReport,
263
+ childText,
264
+ m as classifyModule,
265
+ n as classifyTag,
266
+ o as compareWithBaseline,
267
+ p as cssPropertyOf,
268
+ q as deltaE,
269
+ r as domRendererComponent,
270
+ s as dsEquivalent,
271
+ enclosingTag,
272
+ t as historyPoint,
273
+ u as ignoreDirectives,
274
+ v as importsOf,
275
+ isAuditable,
33
276
  w as isColorLiteral,
34
277
  x as isNonVisual,
35
278
  y as isRouterLink,
@@ -38,11 +281,14 @@ export {
38
281
  E as lineCol,
39
282
  G as lookalikesInClassList,
40
283
  H as markupBlocks,
284
+ missingPieces,
285
+ nameFromClass,
41
286
  J as nativeEquivalent,
42
287
  K as packageName,
43
288
  O as parseAllowlist,
44
289
  P as parseCssColor,
45
290
  Q as planCssFixes,
291
+ ruleClasses,
46
292
  R as scanClassString,
47
293
  S as scanCode,
48
294
  T as scanCssText,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
1
+ {"version":3,"file":"index.js","sources":["../../src/audit/missing-pieces.ts"],"sourcesContent":["// What a product branch introduces that the library lacks: the adoption audit run on each changed product file at the\n// branch head and at its start (the merge base with main), keeping only what the branch added. Each new hardcoded value\n// or foreign component is then mapped to the nearest library target:\n// - values written on a library component (an inline style, or a CSS module class used on it) form one variant of that\n// component: \"Badge: overdue tone\";\n// - a component from another UI library with no library equivalent is a new component;\n// - any other value is a token the library lacks: \"New color #C2410C\", beside the closest token.\n// Values that equal a token, and components the library already has, are fixes (use the library), not pieces.\n// Pure: file texts in, pieces out. Uses the audit, so the TypeScript compiler (syntax only).\nimport type { LayeredTokenSet } from '../layered';\nimport { pieceId, proposedTokenPath, tokenLabel, type MissingPiece, type PieceAspect, type PieceNearest, type PieceValue } from '../prod-branch';\nimport type { DesignSystemIndex } from './ds-index';\nimport { auditFile, isAuditable } from './report';\nimport type { ComponentFinding, StyleFinding, TokenSuggestion } from './types';\n\nexport interface BranchFile {\n /** Repo-relative path. */\n path: string;\n /** Text at the branch start, null when the branch adds the file. */\n before: string | null;\n /** Text at the branch head, null when the branch removes it. */\n after: string | null;\n}\n\nexport interface MissingPiecesResult {\n pieces: MissingPiece[];\n /** New values equal to a token and new non-library components the library has an equivalent for. */\n fixes: { file: string; line: number; what: string; use: string }[];\n /** Files read. */\n files: number;\n}\n\nconst lines = (text: string) => text.split(/\\r?\\n/);\n\n/** Trimmed lines of a text as a multiset: a finding is new when its line was not there before. */\nfunction lineBag(text: string | null): Map<string, number> {\n const bag = new Map<string, number>();\n for (const l of text ? lines(text) : []) {\n const k = l.trim();\n if (k) bag.set(k, (bag.get(k) ?? 0) + 1);\n }\n return bag;\n}\n\nconst offsetOf = (text: string, line: number, column: number): number => {\n let off = 0;\n const ls = text.split('\\n');\n for (let i = 0; i < line - 1 && i < ls.length; i++) off += ls[i]!.length + 1;\n return off + Math.max(0, column - 1);\n};\n\n/** The JSX element whose opening tag holds `offset` (an inline style value), or null. */\nexport function enclosingTag(text: string, offset: number): { name: string; start: number } | null {\n for (let i = offset - 1; i >= Math.max(0, offset - 4000); i--) {\n if (text[i] !== '<' || !/[A-Za-z]/.test(text[i + 1] ?? '')) continue;\n let depth = 0;\n let closed = false;\n for (let j = i + 1; j < offset; j++) {\n const c = text[j];\n if (c === '{') depth++;\n else if (c === '}') depth--;\n else if (c === '>' && depth <= 0 && text[j - 1] !== '=') {\n closed = true;\n break;\n }\n }\n if (closed) return null;\n const m = /^<([A-Za-z][\\w.]*)/.exec(text.slice(i));\n return m ? { name: m[1]!, start: i } : null;\n }\n return null;\n}\n\n/** The plain text right after an opening tag (`<Badge ...>Overdue</Badge>` gives \"Overdue\"), when short. */\nexport function childText(text: string, tagStart: number): string | null {\n let depth = 0;\n for (let j = tagStart + 1; j < text.length; j++) {\n const c = text[j];\n if (c === '{') depth++;\n else if (c === '}') depth--;\n else if (c === '>' && depth <= 0 && text[j - 1] !== '=') {\n if (text[j - 1] === '/') return null;\n const rest = /^([^<{]*)</.exec(text.slice(j + 1));\n const t = rest?.[1]?.trim() ?? '';\n return t && t.split(/\\s+/).length <= 3 && t.length <= 30 ? t : null;\n }\n }\n return null;\n}\n\n/** The classes of the CSS rule a declaration at `offset` belongs to: `.badge.overdue { color: red }` gives [\"badge\", \"overdue\"]. */\nexport function ruleClasses(text: string, offset: number): string[] {\n const open = text.lastIndexOf('{', offset);\n if (open < 0) return [];\n const prev = Math.max(text.lastIndexOf('}', open), text.lastIndexOf(';', open));\n const selector = text.slice(prev + 1, open);\n return [...selector.matchAll(/\\.([A-Za-z_][\\w-]*)/g)].map((m) => m[1]!);\n}\n\nconst GENERIC = new Set(['status', 'badge', 'tag', 'pill', 'chip', 'style', 'styles', 'root', 'wrapper', 'container', 'item', 'label', 'text', 'color', 'bg', 'is', 'has', 'variant', 'tone', 'custom', 'new']);\n\n/** \"badgeOverdue\" on Badge -> \"overdue\"; \"invoice-status--late\" -> \"late\". */\nexport function nameFromClass(cls: string, component?: string): string {\n const words = cls\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .split(/[\\s_-]+/)\n .map((w) => w.toLowerCase())\n .filter(Boolean);\n const comp = component?.toLowerCase();\n const kept = words.filter((w) => w !== comp && !GENERIC.has(w));\n return kept[kept.length - 1] ?? words[words.length - 1] ?? cls;\n}\n\nconst ASPECT: Record<string, PieceAspect> = { color: 'tone', radius: 'shape', spacing: 'size', size: 'size', 'font-size': 'size', 'line-height': 'size' };\n\nfunction aspectOf(values: PieceValue[]): PieceAspect {\n for (const a of ['tone', 'shape', 'size'] as const) if (values.some((v) => ASPECT[v.category] === a)) return a;\n return 'style';\n}\n\nconst norm = (f: { category: string; value: string }) => (f.category === 'color' ? f.value.toLowerCase() : f.value.replace(/\\s+/g, ''));\n\nfunction nearestOf(s: TokenSuggestion | undefined): PieceNearest | undefined {\n if (!s) return undefined;\n return { token: s.token, cssVar: s.cssVar, value: s.value, level: s.level, label: tokenLabel(s.token) };\n}\n\nconst TOKEN_TYPE: Record<string, string> = { color: 'color', duration: 'duration', 'font-weight': 'fontWeight', 'line-height': 'number', 'z-index': 'number', 'font-family': 'fontFamily', shadow: 'shadow', easing: 'cubicBezier' };\n\nconst valueTitle = (category: string, value: string): string => {\n const v = category === 'color' && /^#[0-9a-f]+$/i.test(value) ? value.toUpperCase() : value;\n const word: Record<string, string> = { color: 'color', radius: 'radius', spacing: 'spacing', size: 'size', 'font-size': 'text size', 'font-weight': 'font weight', 'line-height': 'line height', shadow: 'shadow', duration: 'duration', easing: 'easing', 'z-index': 'layer', 'border-width': 'border width', 'font-family': 'font' };\n return `New ${word[category] ?? category} ${v}`;\n};\n\ninterface Hit {\n file: string;\n line: number;\n finding: StyleFinding;\n}\n\n/**\n * The pieces a product branch adds that the library lacks. `files` are the changed files (only auditable ones are\n * read; pass the product's files). `set` lets token pieces name the token files of the closest token (light and dark).\n */\nexport function missingPieces(input: { files: BranchFile[]; ds: DesignSystemIndex; set?: LayeredTokenSet | undefined }): MissingPiecesResult {\n const { ds } = input;\n const files = input.files.filter((f) => f.after !== null && isAuditable(f.path));\n const fixes: MissingPiecesResult['fixes'] = [];\n const variants = new Map<string, { component: string; name: string; hits: Hit[] }>();\n const tokens = new Map<string, { category: string; value: string; name: string | null; hits: Hit[] }>();\n const foreign = new Map<string, { finding: ComponentFinding; hits: { file: string; line: number }[] }>();\n // Classes of CSS modules: which DS component uses each one, read from every changed file at its head.\n const classUse = (cls: string): { component: string; text: string | null } | null => {\n const re = new RegExp(`<([A-Z][\\\\w.]*)\\\\b[^<]*?styles(?:\\\\.${cls}\\\\b|\\\\[['\"]${cls}['\"]\\\\])`);\n for (const f of files) {\n const m = re.exec(f.after!);\n if (m && ds.components.has(m[1]!.split('.')[0]!)) return { component: m[1]!.split('.')[0]!, text: childText(f.after!, m.index) };\n }\n return null;\n };\n\n for (const f of files) {\n const after = f.after!;\n const report = auditFile(f.path, after, ds);\n const before = lineBag(f.before);\n const afterLines = lines(after);\n // Lines the branch added (a line that was there before is not the branch's doing), counted so repeats stay fair.\n const added = (line: number) => {\n const k = afterLines[line - 1]?.trim() ?? '';\n const n = before.get(k) ?? 0;\n if (n > 0) {\n before.set(k, n - 1);\n return false;\n }\n return true;\n };\n const addedLines = new Map<number, boolean>();\n const isNew = (line: number) => {\n if (!addedLines.has(line)) addedLines.set(line, added(line));\n return addedLines.get(line)!;\n };\n const css = report.lang === 'css' || report.lang === 'scss' || report.lang === 'less';\n for (const finding of report.findings) {\n if (!isNew(finding.line)) continue;\n const s = finding.suggestion;\n const at = { file: f.path, line: finding.line };\n if (s?.level === 'exact' || (s?.level === 'high' && finding.category !== 'color')) {\n fixes.push({ ...at, what: finding.value, use: s.token });\n continue;\n }\n const offset = offsetOf(after, finding.line, finding.column);\n let component: string | null = null;\n let name: string | null = null;\n if (css) {\n const classes = ruleClasses(after, offset);\n for (const cls of [...classes].reverse()) {\n const use = classUse(cls);\n if (use) {\n component = use.component;\n name = nameFromClass(cls, use.component);\n break;\n }\n }\n if (!component && classes.length) name = nameFromClass(classes[classes.length - 1]!);\n } else {\n const tag = enclosingTag(after, offset);\n const root = tag?.name.split('.')[0];\n if (tag && root && ds.components.has(root)) {\n component = root;\n name = childText(after, tag.start);\n const cls = /className=\\{?[\"'`]?([\\w-]+)/.exec(after.slice(tag.start, offset))?.[1];\n if (!name && cls) name = nameFromClass(cls, root);\n }\n }\n const hit: Hit = { ...at, finding };\n if (component) {\n const label = (name ?? 'custom').toLowerCase();\n const key = `${component}|${label}`;\n const v = variants.get(key) ?? { component, name: label, hits: [] };\n v.hits.push(hit);\n variants.set(key, v);\n continue;\n }\n const key = `${finding.category}|${norm(finding)}`;\n const t = tokens.get(key) ?? { category: finding.category, value: finding.value, name, hits: [] };\n if (!t.name && name) t.name = name;\n t.hits.push(hit);\n tokens.set(key, t);\n }\n for (const c of report.components) {\n if (!isNew(c.line)) continue;\n if (c.suggestion) {\n fixes.push({ file: f.path, line: c.line, what: c.name, use: c.suggestion });\n continue;\n }\n if (c.kind !== 'foreign') continue;\n const e = foreign.get(c.name) ?? { finding: c, hits: [] };\n e.hits.push({ file: f.path, line: c.line });\n foreign.set(c.name, e);\n }\n }\n\n const where = (hits: { file: string; line: number }[]) => hits.slice(0, 5).map((h) => ({ file: h.file, line: h.line }));\n const pieces: MissingPiece[] = [];\n for (const v of variants.values()) {\n const values: PieceValue[] = [];\n for (const h of v.hits) {\n const pv = { category: h.finding.category, ...(h.finding.property ? { property: h.finding.property } : {}), value: h.finding.value };\n if (!values.some((x) => x.category === pv.category && x.property === pv.property && norm(x) === norm(pv))) values.push(pv);\n }\n const aspect = aspectOf(values);\n const bg = v.hits.find((h) => /background/.test(h.finding.property ?? ''))?.finding.suggestion;\n const nearest = nearestOf(bg ?? v.hits.find((h) => h.finding.suggestion)?.finding.suggestion);\n const shown = values.map((x) => `${x.property ?? x.category} ${x.value}`).join(', ');\n pieces.push({\n id: pieceId([v.component, v.name, aspect]),\n kind: 'variant',\n title: `${v.component}: ${v.name} ${aspect}`,\n summary: `${v.component} restyled by hand (${shown}).${nearest ? ` ${nearest.level === 'high' ? 'It looks a lot like' : 'The closest in the library is'} ${nearest.label}.` : ''}`,\n name: v.name,\n component: v.component,\n aspect,\n values,\n ...(nearest ? { nearest } : {}),\n where: where(v.hits),\n });\n }\n for (const [name, e] of foreign) {\n pieces.push({\n id: pieceId(['component', name]),\n kind: 'component',\n title: `New component: ${name}`,\n summary: `${name} comes from ${e.finding.library}, and the library has nothing like it.`,\n name,\n component: name.split('.').pop()!,\n values: [],\n where: where(e.hits),\n });\n }\n for (const t of tokens.values()) {\n const nearest = nearestOf(t.hits[0]!.finding.suggestion);\n const pname = t.name ?? `${t.category === 'color' ? 'custom' : t.category}`;\n const path = proposedTokenPath(nearest?.token, pname, t.category);\n const tokenFiles = nearest && input.set ? [...new Set(input.set.defs.filter((d) => d.path === nearest.token).map((d) => d.file))] : [];\n const n = t.hits.length;\n pieces.push({\n id: pieceId([t.category, t.name ?? t.value.replace(/^#/, '')]),\n kind: 'token',\n title: valueTitle(t.category, t.value),\n summary: `Written by hand in ${n === 1 ? 'one place' : `${n} places`}.${nearest ? ` ${nearest.level === 'high' ? 'It looks a lot like' : 'The closest in the library is'} ${nearest.label} (${nearest.value}).` : ' The library has nothing close.'}`,\n name: pname,\n values: [{ category: t.category, ...(t.hits[0]!.finding.property ? { property: t.hits[0]!.finding.property } : {}), value: t.value }],\n ...(nearest ? { nearest } : {}),\n where: where(t.hits),\n token: { path, type: TOKEN_TYPE[t.category] ?? 'dimension', value: t.value, files: tokenFiles },\n });\n }\n // Variants and components first (they read as features), then values; ids stay unique.\n const order: Record<MissingPiece['kind'], number> = { variant: 0, component: 1, token: 2 };\n const seen = new Set<string>();\n const out = pieces\n .sort((a, b) => order[a.kind] - order[b.kind])\n .map((p) => {\n let id = p.id;\n for (let i = 2; seen.has(id); i++) id = `${p.id}-${i}`;\n seen.add(id);\n return id === p.id ? p : { ...p, id };\n });\n return { pieces: out, fixes, files: files.length };\n}\n"],"names":["l","k","j","c","m","t","w","v","f","s","n","key","e","h","x","d","b","p"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { LayeredTokenSet } from '../layered';
2
+ import { type MissingPiece } from '../prod-branch';
3
+ import type { DesignSystemIndex } from './ds-index';
4
+ export interface BranchFile {
5
+ /** Repo-relative path. */
6
+ path: string;
7
+ /** Text at the branch start, null when the branch adds the file. */
8
+ before: string | null;
9
+ /** Text at the branch head, null when the branch removes it. */
10
+ after: string | null;
11
+ }
12
+ export interface MissingPiecesResult {
13
+ pieces: MissingPiece[];
14
+ /** New values equal to a token and new non-library components the library has an equivalent for. */
15
+ fixes: {
16
+ file: string;
17
+ line: number;
18
+ what: string;
19
+ use: string;
20
+ }[];
21
+ /** Files read. */
22
+ files: number;
23
+ }
24
+ /** The JSX element whose opening tag holds `offset` (an inline style value), or null. */
25
+ export declare function enclosingTag(text: string, offset: number): {
26
+ name: string;
27
+ start: number;
28
+ } | null;
29
+ /** The plain text right after an opening tag (`<Badge ...>Overdue</Badge>` gives "Overdue"), when short. */
30
+ export declare function childText(text: string, tagStart: number): string | null;
31
+ /** The classes of the CSS rule a declaration at `offset` belongs to: `.badge.overdue { color: red }` gives ["badge", "overdue"]. */
32
+ export declare function ruleClasses(text: string, offset: number): string[];
33
+ /** "badgeOverdue" on Badge -> "overdue"; "invoice-status--late" -> "late". */
34
+ export declare function nameFromClass(cls: string, component?: string): string;
35
+ /**
36
+ * The pieces a product branch adds that the library lacks. `files` are the changed files (only auditable ones are
37
+ * read; pass the product's files). `set` lets token pieces name the token files of the closest token (light and dark).
38
+ */
39
+ export declare function missingPieces(input: {
40
+ files: BranchFile[];
41
+ ds: DesignSystemIndex;
42
+ set?: LayeredTokenSet | undefined;
43
+ }): MissingPiecesResult;
package/dist/config.d.ts CHANGED
@@ -4,6 +4,19 @@ import { type Rights, type VisualImpactConfig } from './rights';
4
4
  import { type ProductConfig } from './products';
5
5
  /** File name Bridge looks for at the repository root. */
6
6
  export declare const CONFIG_FILE = "bridge.config.json";
7
+ /**
8
+ * How Bridge sends a change to the code. "suggest": a GitHub issue that holds the exact change (suggestions.ts);
9
+ * owners approve it and a developer applies it with their own access. "direct": Bridge opens branches and pull
10
+ * requests itself. Absent from the file: "suggest".
11
+ */
12
+ export type ChangesMode = 'suggest' | 'direct';
13
+ export declare const CHANGES_MODES: readonly ChangesMode[];
14
+ export declare const DEFAULT_CHANGES_MODE: ChangesMode;
15
+ /** The deployed Storybook Bridge reads the component list and previews from. */
16
+ export interface StorybookConfig {
17
+ /** Root URL (where index.json and iframe.html are served), https only, no trailing slash. */
18
+ url: string;
19
+ }
7
20
  export interface RepoRef {
8
21
  owner: string;
9
22
  name: string;
@@ -154,6 +167,10 @@ export interface BridgeConfig {
154
167
  visualImpact?: VisualImpactConfig | undefined;
155
168
  /** Apps that consume the design system, the default one being the library (see products.ts). Default DEFAULT_PRODUCTS. */
156
169
  products: ProductConfig[];
170
+ /** How changes reach the code: suggestions (default) or branches and pull requests. See changesMode(). */
171
+ changes: ChangesMode;
172
+ /** The Storybook set in the file. Read it through storybookUrl(), which falls back to previews.production.storybook. */
173
+ storybook?: StorybookConfig | undefined;
157
174
  }
158
175
  type Partialish<T> = {
159
176
  [K in keyof T]?: T[K] | undefined;
@@ -214,6 +231,10 @@ export interface BridgeConfigInput {
214
231
  integrations?: IntegrationsConfig;
215
232
  visualImpact?: Partial<VisualImpactConfig>;
216
233
  products?: ProductConfig[];
234
+ changes?: ChangesMode;
235
+ storybook?: {
236
+ url: string;
237
+ };
217
238
  }
218
239
  export declare const DEFAULT_TOKEN_PATHS: TokenPaths;
219
240
  export declare const DEFAULT_GOVERNANCE: GovernanceConfig;
@@ -231,6 +252,13 @@ export declare const tokensPath: (config: BridgeConfig, rel?: string) => string;
231
252
  export declare const protectedBranchesOf: (config: BridgeConfig) => string[];
232
253
  /** The base branch proposals target. */
233
254
  export declare const baseBranchOf: (config: BridgeConfig) => string;
255
+ /** How changes reach the code for this repo: the config's mode ("suggest" when the file does not say). */
256
+ export declare const changesMode: (config: Pick<BridgeConfig, "changes"> | null | undefined) => ChangesMode;
257
+ /**
258
+ * The Storybook to read: `storybook.url`, else the production Storybook of `previews` when it is https. Undefined
259
+ * when neither is set. No trailing slash.
260
+ */
261
+ export declare function storybookUrl(config: Pick<BridgeConfig, 'storybook' | 'previews'> | null | undefined): string | undefined;
234
262
  /** Plain-JSON form of a config, in the canonical shape (what `bridge init` writes). */
235
263
  export declare function toConfigJson(config: BridgeConfig, schema?: string): Record<string, unknown>;
236
264
  export {};